package com.pnikosis.materialishprogress;

import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.Arc;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.utils.Color;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.miscservices.timeutility.Time;

/**
 * A Material style progress wheel, compatible up to 2.2.
 * Todd Davies' Progress Wheel https://github.com/Todd-Davies/ProgressWheel
 *
 * @author Nico Hormazábal
 *         <p/>
 *         Licensed under the Apache License 2.0 license see:
 *         http://www.apache.org/licenses/LICENSE-2.0
 */
public class ProgressWheel extends Component {
    private static final String TAG = ProgressWheel.class.getSimpleName();
    private final int barLength = 16;
    private final int barMaxLength = 270;
    private final long pauseGrowingTime = 200;
    /**
     * *********
     * DEFAULTS *
     * **********
     */

    // Sizes (with defaults in DP)
    private int circleRadius = dp2px(28);

    private int barWidth = dp2px(4);
    private int rimWidth = dp2px(4);
    private boolean fillRadius = false;
    private double timeStartGrowing = 0;
    private double barSpinCycleTime = 460;
    private float barExtraLength = 0;
    private boolean barGrowingFromFront = true;
    private long pausedTimeWithoutGrowing = 0;

    private int barColor = 0xAA000000;
    private int rimColor = 0x00FFFFFF;

    // Paints
    private Paint barPaint = new Paint();
    private Paint rimPaint = new Paint();

    // Rectangles
    private RectFloat circleBounds = new RectFloat();

    // Animation
    // The amount of degrees per second
    private float spinSpeed = 230.0f;

    // The last time the spinner was animated
    private long lastTimeAnimated = 0;

    private boolean linearProgress;

    private float mProgress = 0.0f;
    private float mTargetProgress = 0.0f;
    private boolean isSpinning = false;

    private ProgressCallback callback;

    private AnimatorValue spinAnimator;

    private boolean shouldAnimate = true;

    /**
     * The constructor for the ProgressWheel
     */
    public ProgressWheel(Context context) {
        this(context, null, null);
    }

    public ProgressWheel(Context context, AttrSet attrs) {
        this(context, attrs, null);
    }

    public ProgressWheel(Context context, AttrSet attrs, String defStyle) {
        super(context, attrs, defStyle);
        parseAttributes(attrs);
        setAnimationEnabled();
        setupPaints();
        onDraw();
    }

    private void setAnimationEnabled() {
        shouldAnimate = true;
    }

    /**
     * Set the properties of the paints we're using to
     * draw the progress wheel
     */
    private void setupPaints() {
        barPaint.setColor(new Color(barColor));
        barPaint.setAntiAlias(true);
        barPaint.setStyle(Paint.Style.STROKE_STYLE);
        barPaint.setStrokeWidth(barWidth);

        rimPaint.setColor(new Color(rimColor));
        rimPaint.setAntiAlias(true);
        rimPaint.setStyle(Paint.Style.STROKE_STYLE);
        rimPaint.setStrokeWidth(rimWidth);
    }

    private void setupBounds(int layout_width, int layout_height) {
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();

        if (!fillRadius) {
            // Width should equal to Height, find the min value to setup the circle
            int minValue =
                    Math.min(layout_width - paddingLeft - paddingRight, layout_height - paddingBottom - paddingTop);

            int circleDiameter = Math.min(minValue, circleRadius * 2 - barWidth * 2);

            // Calc the Offset if needed for centering the wheel in the available space
            int xOffset = (layout_width - paddingLeft - paddingRight - circleDiameter) / 2 + paddingLeft;
            int yOffset = (layout_height - paddingTop - paddingBottom - circleDiameter) / 2 + paddingTop;

            circleBounds =
                    new RectFloat(
                            xOffset + barWidth,
                            yOffset + barWidth,
                            xOffset + circleDiameter - barWidth,
                            yOffset + circleDiameter - barWidth);
        } else {
            circleBounds =
                    new RectFloat(
                            paddingLeft + barWidth,
                            paddingTop + barWidth,
                            layout_width - paddingRight - barWidth,
                            layout_height - paddingBottom - barWidth);
        }
    }

    /**
     * Parse the attributes passed to the view from the XML
     *
     * @param attrs the attributes to parse
     */
    private void parseAttributes(AttrSet attrs) {
        circleRadius = AttrUtils.getDimensionFromAttr(attrs, "matProg_circleRadius", circleRadius);
        fillRadius = AttrUtils.getBooleanFromAttr(attrs, "matProg_fillRadius", false);
        barWidth = AttrUtils.getDimensionFromAttr(attrs, "matProg_barWidth", barWidth);
        rimWidth = AttrUtils.getDimensionFromAttr(attrs, "matProg_rimWidth", rimWidth);

        float baseSpinSpeed = AttrUtils.getFloatFromAttr(attrs, "matProg_spinSpeed", spinSpeed / 360.0f);
        spinSpeed = baseSpinSpeed * 360;

        barSpinCycleTime = AttrUtils.getIntFromAttr(attrs, "matProg_barSpinCycleTime", (int) barSpinCycleTime);

        barColor = AttrUtils.getColorFromAttr(attrs, "matProg_barColor", barColor);
        rimColor = AttrUtils.getColorFromAttr(attrs, "matProg_rimColor", rimColor);
        linearProgress = AttrUtils.getBooleanFromAttr(attrs, "matProg_linearProgress", false);

        if (AttrUtils.getBooleanFromAttr(attrs, "matProg_progressIndeterminate", false)) {
            spin();
        }
    }

    public void setCallback(ProgressCallback progressCallback) {
        callback = progressCallback;

        if (!isSpinning) {
            runCallback();
        }
    }

    // ----------------------------------
    // Animation stuff
    // ----------------------------------

    private void onDraw() {
        addDrawTask(
                new DrawTask() {
                    @Override
                    public void onDraw(Component component, Canvas canvas) {
                        setupBounds(getWidth(), getHeight());
                        Arc arc = new Arc();
                        arc.setArc(360, 360, false);
                        canvas.drawArc(circleBounds, arc, rimPaint);
                        boolean mustInvalidate = false;

                        if (!shouldAnimate) {
                            return;
                        }

                        if (isSpinning) {
                            // Draw the spinning bar
                            mustInvalidate = true;
                            long deltaTime = (Time.getRealActiveTime() - lastTimeAnimated);
                            float deltaNormalized = deltaTime * spinSpeed / 1000.0f;
                            updateBarLength(deltaTime);
                            mProgress += deltaNormalized;
                            if (mProgress > 360) {
                                mProgress -= 360f;
                                runCallback(-1.0f);
                            }
                            lastTimeAnimated = Time.getRealActiveTime();

                            float from = mProgress - 90;
                            float length = barLength + barExtraLength;
                            canvas.drawArc(circleBounds, new Arc(from, length, false), barPaint);
                        } else {
                            float oldProgress = mProgress;

                            if (mProgress != mTargetProgress) {
                                // We smoothly increase the progress bar
                                mustInvalidate = true;

                                float deltaTime = (float) (Time.getRealActiveTime() - lastTimeAnimated) / 1000;
                                float deltaNormalized = deltaTime * spinSpeed;

                                mProgress = Math.min(mProgress + deltaNormalized, mTargetProgress);
                                lastTimeAnimated = Time.getRealActiveTime();
                            }

                            if (oldProgress != mProgress) {
                                runCallback();
                            }

                            float offset = 0.0f;
                            float progress = mProgress;
                            if (!linearProgress) {
                                float factor = 2.0f;
                                offset = (float) (1.0f - Math.pow(1.0f - mProgress / 360.0f, 2.0f * factor)) * 360.0f;
                                progress = (float) (1.0f - Math.pow(1.0f - mProgress / 360.0f, factor)) * 360.0f;
                            }
                            canvas.drawArc(circleBounds, new Arc(offset - 90, progress, false), barPaint);
                        }

                        if (mustInvalidate) {
                            startAnimator();
                        } else {
                            stopAnimator();
                        }
                    }
                });
    }

    private void updateBarLength(long deltaTimeInMilliSeconds) {
        if (pausedTimeWithoutGrowing >= pauseGrowingTime) {
            timeStartGrowing += deltaTimeInMilliSeconds;

            if (timeStartGrowing > barSpinCycleTime) {
                // We completed a size change cycle
                // (growing or shrinking)
                timeStartGrowing -= barSpinCycleTime;
                // if(barGrowingFromFront) {
                pausedTimeWithoutGrowing = 0;
                // }
                barGrowingFromFront = !barGrowingFromFront;
            }

            float distance = (float) Math.cos((timeStartGrowing / barSpinCycleTime + 1) * Math.PI) / 2 + 0.5f;
            float destLength = (barMaxLength - barLength);

            if (barGrowingFromFront) {
                barExtraLength = distance * destLength;
            } else {
                float newLength = destLength * (1 - distance);
                mProgress += (barExtraLength - newLength);
                barExtraLength = newLength;
            }
        } else {
            pausedTimeWithoutGrowing += deltaTimeInMilliSeconds;
        }
    }

    /**
     * is spinning
     * @return Check if the wheel is currently spinning
     */
    public boolean isSpinning() {
        return isSpinning;
    }

    /**
     * Reset the count (in increment mode)
     */
    public void resetCount() {
        mProgress = 0.0f;
        mTargetProgress = 0.0f;
        invalidate();
    }

    /**
     * Turn off spin mode
     */
    public void stopSpinning() {
        isSpinning = false;
        mProgress = 0.0f;
        mTargetProgress = 0.0f;
        invalidate();
    }

    private void startAnimator() {
        if (spinAnimator == null) {
            spinAnimator = new AnimatorValue();
            spinAnimator.setLoopedCount(-1);
            spinAnimator.setValueUpdateListener(
                    new AnimatorValue.ValueUpdateListener() {
                        @Override
                        public void onUpdate(AnimatorValue animatorValue, float v) {
                            invalidate();
                        }
                    });
            spinAnimator.start();
        }
    }

    private void stopAnimator() {
        if (spinAnimator != null) {
            spinAnimator.stop();
            spinAnimator = null;
        }
    }

    /**
     * Puts the view on spin mode
     */
    public void spin() {
        lastTimeAnimated = Time.getRealActiveTime();
        isSpinning = true;
        invalidate();
    }

    private void runCallback(float value) {
        if (callback != null) {
            callback.onProgressUpdate(value);
        }
    }

    private void runCallback() {
        if (callback != null) {
            float normalizedProgress = (float) Math.round(mProgress * 100 / 360.0f) / 100;
            callback.onProgressUpdate(normalizedProgress);
        }
    }

    /**
     * Set the progress to a specific value,
     * the bar will be set instantly to that value
     *
     * @param progress the progress between 0 and 1
     */
    public void setInstantProgress(float progress) {
        if (isSpinning) {
            mProgress = 0.0f;
            isSpinning = false;
        }

        if (progress > 1.0f) {
            progress -= 1.0f;
        } else if (progress < 0) {
            progress = 0;
        }

        if (progress == mTargetProgress) {
            return;
        }

        mTargetProgress = Math.min(progress * 360.0f, 360.0f);
        mProgress = mTargetProgress;
        lastTimeAnimated = Time.getRealActiveTime();
        invalidate();
    }

    /**
     * get progress
     * @return the current progress between 0.0 and 1.0,if the wheel is indeterminate, then the result is -1
     */
    public float getProgress() {
        return isSpinning ? -1 : mProgress / 360.0f;
    }

    // ----------------------------------
    // Getters + setters
    // ----------------------------------

    /**
     * Set the progress to a specific value,
     * the bar will smoothly animate until that value
     *
     * @param progress the progress between 0 and 1
     */
    public void setProgress(float progress) {
        if (isSpinning) {
            mProgress = 0.0f;
            isSpinning = false;

            runCallback();
        }

        if (progress > 1.0f) {
            progress -= 1.0f;
        } else if (progress < 0) {
            progress = 0;
        }

        if (progress == mTargetProgress) {
            return;
        }

        // If we are currently in the right position
        // we set again the last time animated so the
        // animation starts smooth from here
        if (mProgress == mTargetProgress) {
            lastTimeAnimated = Time.getRealActiveTime();
        }

        mTargetProgress = Math.min(progress * 360.0f, 360.0f);

        invalidate();
    }

    /**
     * Sets the determinate progress mode
     *
     * @param isLinear if the progress should increase linearly
     */
    public void setLinearProgress(boolean isLinear) {
        linearProgress = isLinear;
        if (!isSpinning) {
            invalidate();
        }
    }

    /**
     * get circle radius
     * @return the radius of the wheel in pixels
     */
    public int getCircleRadius() {
        return circleRadius;
    }

    /**
     * Sets the radius of the wheel
     *
     * @param circleRadius the expected radius, in pixels
     */
    public void setCircleRadius(int circleRadius) {
        this.circleRadius = circleRadius;
        if (!isSpinning) {
            invalidate();
        }
    }

    /**
     * get bar width
     * @return the width of the spinning bar
     */
    public int getBarWidth() {
        return barWidth;
    }

    /**
     * Sets the width of the spinning bar
     *
     * @param barWidth the spinning bar width in pixels
     */
    public void setBarWidth(int barWidth) {
        this.barWidth = barWidth;
        setupPaints();
        if (!isSpinning) {
            invalidate();
        }
    }

    /**
     * get bar color
     * @return the color of the spinning bar
     */
    public int getBarColor() {
        return barColor;
    }

    /**
     * Sets the color of the spinning bar
     *
     * @param barColor The spinning bar color
     */
    public void setBarColor(int barColor) {
        this.barColor = barColor;
        setupPaints();
        if (!isSpinning) {
            invalidate();
        }
    }

    /**
     * get rim color
     * @return the color of the wheel's contour
     */
    public int getRimColor() {
        return rimColor;
    }

    /**
     * Sets the color of the wheel's contour
     *
     * @param rimColor the color for the wheel
     */
    public void setRimColor(int rimColor) {
        this.rimColor = rimColor;
        setupPaints();
        if (!isSpinning) {
            invalidate();
        }
    }

    /**
     * get spin speed
     * @return the base spinning speed, in full circle turns per second
     * (1.0 equals on full turn in one second), this value also is applied for
     * the smoothness when setting a progress
     */
    public float getSpinSpeed() {
        return spinSpeed / 360.0f;
    }

    /**
     * Sets the base spinning speed, in full circle turns per second
     * (1.0 equals on full turn in one second), this value also is applied for
     * the smoothness when setting a progress
     *
     * @param spinSpeed the desired base speed in full turns per second
     */
    public void setSpinSpeed(float spinSpeed) {
        this.spinSpeed = spinSpeed * 360.0f;
    }

    /**
     * get rim width
     * @return the width of the wheel's contour in pixels
     */
    public int getRimWidth() {
        return rimWidth;
    }

    /**
     * Sets the width of the wheel's contour
     *
     * @param rimWidth the width in pixels
     */
    public void setRimWidth(int rimWidth) {
        this.rimWidth = rimWidth;
        setupPaints();
        if (!isSpinning) {
            invalidate();
        }
    }

    public interface ProgressCallback {
        /**
         * Method to call when the progress reaches a value
         * in order to avoid float precision issues, the progress
         * is rounded to a float with two decimals.
         *
         * In indeterminate mode, the callback is called each time
         * the wheel completes an animation cycle, with, the progress value is -1.0f
         *
         * @param progress a double value between 0.00 and 1.00 both included
         */
        public void onProgressUpdate(float progress);
    }

    private int dp2px(float dp) {
        return (int) (getResourceManager().getDeviceCapability().screenDensity / 160 * dp);
    }
}
